﻿using System;
using System.Reflection;
using System.Runtime.InteropServices.Expando;
using System.Runtime.InteropServices.ComTypes;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Drawing;
using System.Drawing.Imaging;
using System.Threading;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Net;
using dshow;
using dshow.Core;
using System.Linq;
using System.Text;
using BrainTechLLC.ImageOperatorsNonAForgeDependent;
using BrainTechLLC;
using System.Configuration;
using System.Diagnostics;
using BrainTechLLC.ThreadSafeObjects;
using System.Windows.Forms;
using System.Windows.Threading;

namespace BrainTechLLC.WebCamLib3
{
    public class VideoCapture : BaseNotifyPropertyChanged, IVideoSource, ISampleGrabberCB, IDisposable, INotifyPropertyChanged, IVideoFeed
    {
        protected ThreadSafeQueue<FrameInfo> _incomingFrameQueue = new ThreadSafeQueue<FrameInfo>();
        protected int TimesNoItemsWereInQueue;
        protected double _lastImageSampleTime = 0.0;
        protected string source = "Webcam";
        protected string name = "";
        protected string _cameraName = "";
        public int _incomingVideoWidth = -1;
        public int _incomingVideoHeight = -1;
        protected string _cameraMoniker = "No Webcam";
        protected Thread _cameraCaptureThread = null;
        protected ManualResetEvent _stopCameraEvent = null;
        protected double _minimumTimeBetweenHardwareFrames = 0.065;
        public bool _isProcessing = false;
        public Exception _cameraError = null;

        public ThreadSafeQueue<FrameInfo> IncomingFrameQueue 
        {
            get { return _incomingFrameQueue; }
        }
        
        [DllImport("ntdll.dll")]
        private static extern unsafe byte* memcpy(byte* dst, byte* src, int count);

        [Category("[Video Stats]"), Description(""), DisplayName("Local Webcam Identifier"), ApplicationScopedSetting()]
        public string CameraMoniker
        {
            get { return _cameraMoniker; }
            protected set
            {
                if (_cameraMoniker != value)
                {
                    _cameraMoniker = value;
                    OnPropertyChanged("CameraMoniker");
                }
            }
        }

        // Get state of the video source thread
        public bool CameraCaptureRunning
        {
            get
            {
                if (_cameraCaptureThread != null)
                {
                    if (_cameraCaptureThread.Join(0) == false)
                        return true;

                    // the thread is not running, so free resources
                    FreeCameraCaptureThread();
                }
                return false;
            }
        }

        public int IncomingFrameQueueCount
        {
            get { return IncomingFrameQueue.Count; }
        }

        [Category("[Video Stats]"), Description(""), DisplayName("Local Webcam Name"), ApplicationScopedSetting()]
        public string CameraName
        {
            get { return _cameraName; }
            protected set
            {
                if (_cameraName != value)
                {
                    _cameraName = value;
                    OnPropertyChanged("CameraName");
                }
            }
        }

        [Category("[Video]"), Description(""), ApplicationScopedSetting()]
        public double MinimumTimeBetweenHardwareFrames
        {
            get { return _minimumTimeBetweenHardwareFrames; }
            set
            {
                if (_minimumTimeBetweenHardwareFrames != value)
                {
                    _minimumTimeBetweenHardwareFrames = value;
                    OnPropertyChanged("MinimumTimeBetweenHardwareFrames");
                }
            }
        }

        public int IncomingVideoWidth
        {
            get { return _incomingVideoWidth; }
            protected set
            {
                if (_incomingVideoWidth != value)
                {
                    _incomingVideoWidth = value;
                    OnPropertyChanged("IncomingVideoWidth");
                }
            }
        }

        public int IncomingVideoHeight
        {
            get { return _incomingVideoHeight; }
            protected set
            {
                if (_incomingVideoHeight != value)
                {
                    _incomingVideoHeight = value;
                    OnPropertyChanged("IncomingVideoHeight");
                }
            }
        }

        public bool IsProcessing
        {
            get { return _isProcessing; }
            protected set
            {
                if (_isProcessing != value)
                {
                    _isProcessing = value;
                    OnPropertyChanged("IsProcessing");
                }
            }
        }

        public Exception CameraError
        {
            get { return _cameraError; }
            protected set
            {
                if (_cameraError != value)
                {
                    _cameraError = value;
                    OnPropertyChanged("CameraError");
                }
            }
        }

        [Category("[Program Control]"), Description("Thread priority of the video capture thread"), DisplayName("Video Capture Priority")]
        public ThreadPriority CameraCaptureThreadPriority
        {
            get
            {
                if (_cameraCaptureThread != null)
                    return _cameraCaptureThread.Priority;

                return ThreadPriority.Normal;
            }
            set
            {
                if (_cameraCaptureThread != null)
                {
                    try
                    {
                        _cameraCaptureThread.Priority = value;
                        OnPropertyChanged("CameraCaptureThreadPriority");
                    }
                    catch { }
                }
            }
        }

        public void Dispose()
        {
            try
            {
                StopCameraCapture();
            }
            catch { }
        }

        public FrameInfo GetNextFrame()
        {
            FrameInfo frameInfo;

            if (IncomingFrameQueue.Count > 10)
            {
                FrameInfo[] tossers = new FrameInfo[8];
                int dequeued = IncomingFrameQueue.DequeueMultiple(tossers, 8);

                for (int n = 0; n < dequeued; n++)
                {
                    FrameInfo toToss = tossers[n];
                    toToss.ClearBitmapAndBytes();
                }
            }

            if (!IncomingFrameQueue.Dequeue(out frameInfo))
            {
                return null;
            }

            OnPropertyChanged("IncomingFrameQueueCount");
            return frameInfo;
        }

        private bool AssignCameraNameAndMoniker(string cameraName, string moniker)
        {
            this.name = cameraName;
            this.CameraName = cameraName;
            this.CameraMoniker = moniker;
            this.source = moniker;
            return true;
        }

        public void ClearVideoQueue()
        {
            IncomingFrameQueue.Clear();
        }

        // Close video source
        public void StopCameraCapture()
        {
            if (_cameraCaptureThread != null)
            {
                SignalCameraToStop();
                Thread.Sleep(300);

                TerminateCameraCapture();
            }
            ClearVideoQueue();

            IncomingVideoWidth = -1;
            IncomingVideoHeight = -1;
        }

        // Start video source
        public bool StartCameraCapture()
        {
            return StartCameraCapture(ThreadPriority.Normal, _minimumTimeBetweenHardwareFrames);
        }

        // Start work
        public bool StartCameraCapture(ThreadPriority cameraCapturePriority, double minimumTimeBetweenHardwareFrames)
        {
            if (_cameraCaptureThread != null)
                return false;

            _minimumTimeBetweenHardwareFrames = minimumTimeBetweenHardwareFrames;
            _stopCameraEvent = new ManualResetEvent(false);
            _cameraCaptureThread = new Thread(new ThreadStart(WorkerThread));
            _cameraCaptureThread.IsBackground = true;
            _cameraCaptureThread.Name = source;
            _cameraCaptureThread.Priority = cameraCapturePriority;
            _cameraCaptureThread.Start();
            return true;
        }

        public int SampleCB(double SampleTime, IntPtr pSample)
        {
            return 0;
        }

        // Callback method that receives a pointer to the sample buffer
        public int BufferCB(double sampleTime, IntPtr pBuffer, int bufferLen)
        {
            if (_incomingVideoWidth < 0)
            {
                return 0;
            }

            if (_lastImageSampleTime > 0 && sampleTime - _lastImageSampleTime < _minimumTimeBetweenHardwareFrames)
            {
                if (IncomingFrameQueue.Count > 2)
                {
                    _minimumTimeBetweenHardwareFrames += 0.005;
                }

                return 0;
            }

            // both widths?
            int nStrideGuess = (_incomingVideoWidth * 3) + ((_incomingVideoWidth * 3) % 4);
            int imageSize = (nStrideGuess * _incomingVideoHeight);

            PixelFormat pixelFormat = PixelFormat.Format24bppRgb;

            FrameInfo frameInfo = new FrameInfo()
            {
                _bytes = new byte[imageSize],
                ByteLength = imageSize,
                StrideGuess = nStrideGuess,
                CameraMoniker = this.CameraMoniker,
                CameraName = this.CameraName,
                Height = _incomingVideoHeight,
                Width = _incomingVideoWidth,
                ImageFrameTime = sampleTime,
                PixelFormat = pixelFormat
            };

            unsafe
            {
                fixed (byte* pb = &(frameInfo._bytes[0]))
                {
                    memcpy(pb, (byte*)pBuffer.ToPointer(), imageSize);
                }
            }

            // we may want to use TryAquireLock since we're in a thread called into by a webcam/com
            int itemsNowInQueue = IncomingFrameQueue.EnqueueIfPossible(frameInfo);

            if (itemsNowInQueue == 1 && _minimumTimeBetweenHardwareFrames > 0.0005)
            {
                if (++TimesNoItemsWereInQueue > 5)
                {
                    _minimumTimeBetweenHardwareFrames = Math.Max(0.0005, _minimumTimeBetweenHardwareFrames / 2.0);
                    TimesNoItemsWereInQueue = 0;
                }
            }
            else
            {
                TimesNoItemsWereInQueue = 0;
            }

            if (sampleTime > _lastImageSampleTime)
            {
                _lastImageSampleTime = sampleTime;
            }

            return 0;
        }

        // Signal thread to stop work
        public void SignalCameraToStop()
        {
            if (_cameraCaptureThread != null)
            {
                _stopCameraEvent.Set();
            }
        }

        // Wait for thread stop
        public void WaitForCameraToStop()
        {
            if (_cameraCaptureThread != null)
            {
                _cameraCaptureThread.Join();
                FreeCameraCaptureThread();
            }
        }

        // Abort thread
        public void TerminateCameraCapture()
        {
            if (this.CameraCaptureRunning)
            {
                _cameraCaptureThread.Abort();
            }
        }

        // Free resources
        public void FreeCameraCaptureThread()
        {
            _cameraCaptureThread = null;
            _stopCameraEvent.Close();
            _stopCameraEvent = null;
        }

        // Thread entry point
        unsafe private void WorkerThread()
        {
            OnPropertyChanged("CameraCaptureRunning");
            IsProcessing = false;
            CameraError = null;

            // objects
            object graphObj = null;
            object sourceObj = null;
            object grabberObj = null;

            // interfaces

            //IGraphBuilder graph = null;
            IGraphBuilder graph = null;
            IBaseFilter sourceBase = null;
            IBaseFilter grabberBase = null;
            ISampleGrabber sg = null;
            IMediaControl mc = null;

            try
            {
                // Get type for filter graph
                Type srvType = Type.GetTypeFromCLSID(Clsid.FilterGraph);

                // create filter graph
                graphObj = Activator.CreateInstance(srvType);
                graph = (IGraphBuilder)graphObj;

                // ----
                UCOMIBindCtx bindCtx = null;
                UCOMIMoniker moniker = null;
                int n = 0;

                // create bind context        

                if (Win32.CreateBindCtx(0, out bindCtx) == 0)
                {
                    // convert moniker`s string to a moniker
                    if (Win32.MkParseDisplayName(bindCtx, source, ref n, out moniker) == 0)
                    {
                        // get device base filter
                        Guid filterId = typeof(IBaseFilter).GUID;
                        moniker.BindToObject(null, null, ref filterId, out sourceObj);

                        Marshal.ReleaseComObject(moniker);
                        moniker = null;
                    }
                    Marshal.ReleaseComObject(bindCtx);
                    bindCtx = null;
                }
                // ----

                if (sourceObj == null)
                    throw new ApplicationException("Failed to instantiate the device interface");

                sourceBase = (IBaseFilter)sourceObj;
                // Get type for sample grabber
                srvType = Type.GetTypeFromCLSID(Clsid.SampleGrabber);

                //if (srvType == null)
                //  throw new ApplicationException("Failed creating video stream sample grabber object");

                // create sample grabber
                grabberObj = Activator.CreateInstance(srvType);
                sg = (ISampleGrabber)grabberObj;
                grabberBase = (IBaseFilter)grabberObj;

                int nVidWidth = 400;
                int nVidHeight = 300;
                //int nVidWidth = 640;
                //int nVidHeight = 480;

                // add source filter to graph
                IGraphBuilder gr = (IGraphBuilder)graph;
                gr.AddFilter(sourceBase, "source");
                gr.AddFilter(grabberBase, "grabber");

                AMMediaType pmt = new AMMediaType();
                IntPtr ipmt = Marshal.AllocCoTaskMem(128);
                Marshal.StructureToPtr(pmt, ipmt, false);

                //IntPtr ipmt = Marshal.AllocCoTaskMem(128); //66);
                //AMMediaType pmt = (AMMediaType)Marshal.PtrToStructure(ipmt, typeof(AMMediaType));

                FreeAMMediaType(pmt);
                // Begin new
                VideoInfoHeader vih = new VideoInfoHeader();
                vih.AvgTimePerFrame = 10000000; // 0
                //BitmapInfoHeader bmi = new BitmapInfoHeader();
                vih.BmiHeader = new BitmapInfoHeader();
                vih.BmiHeader.Size = 40;
                vih.BmiHeader.Width = nVidWidth;
                // height negative means image top-down
                vih.BmiHeader.Height = nVidHeight;
                vih.BmiHeader.Planes = 1;
                vih.BmiHeader.BitCount = 24;
                vih.BmiHeader.Compression = 0; // 0 = BI_RGB
                vih.BmiHeader.ImageSize = nVidWidth * nVidHeight * 3;
                pmt.formatSize = 88;
                IntPtr vidBuffer = Marshal.AllocCoTaskMem(88);
                Marshal.StructureToPtr(vih, vidBuffer, false);
                pmt.formatPtr = vidBuffer;
                Marshal.StructureToPtr(pmt, ipmt, false);
                pmt.majorType = MediaType.Video;
                pmt.subType = MediaSubType.RGB24;
                pmt.formatType = new Guid(0x05589f80, 0xc356, 0x11ce, 0xbf, 0x01, 0x00, 0xaa, 0x00, 0x55, 0x59, 0x5a);
                pmt.temporalCompression = false; // was false
                pmt.sampleSize = nVidWidth * nVidHeight * 3;
                //pmt.fixedSizeSamples = true; // was not set        
                sg.SetBufferSamples(false);
                sg.SetOneShot(false);

                int nRet = sg.SetMediaType(pmt);

                // connect pins
                if (gr.Connect(DSTools.GetOutPin(sourceBase, 0), DSTools.GetInPin(grabberBase, 0)) < 0)
                    throw new ApplicationException("Could not connect with webcam input stream");

                // get media type
                if (sg.GetConnectedMediaType(pmt) == 0)
                {
                    VideoInfoHeader vih2 = (VideoInfoHeader)Marshal.PtrToStructure(pmt.formatPtr, typeof(VideoInfoHeader));
                    nVidWidth = vih2.BmiHeader.Width;
                    nVidHeight = vih2.BmiHeader.Height;
                    pmt.sampleSize = nVidWidth * nVidHeight * 3;
                    vih2.BmiHeader.ImageSize = nVidWidth * nVidHeight * 3;
                    vih2.AvgTimePerFrame = 10000000;
                    //vih2.BmiHeader.BitCount 
                    Marshal.StructureToPtr(vih2, pmt.formatPtr, false);
                    nRet = sg.SetMediaType(pmt);
                    sg.GetConnectedMediaType(pmt);
                    vih2 = (VideoInfoHeader)Marshal.PtrToStructure(pmt.formatPtr, typeof(VideoInfoHeader));
                    _incomingVideoWidth = vih2.BmiHeader.Width;
                    _incomingVideoHeight = vih2.BmiHeader.Height;
                    FreeAMMediaType(pmt);
                    pmt = null;
                }

                // render
                int nRetGraph = gr.Render(DSTools.GetOutPin(grabberBase, 0));
                sg.SetCallback(this, 1);

                // window
                IVideoWindow win = (IVideoWindow)graphObj;
                //win.put_Width(640);
                //win.put_Height(480);
                //win.put_FullScreenMode(true);        
                //win.put_AutoShow(true);

                win.put_AutoShow(false);
                win = null;

                // get media control
                mc = (IMediaControl)graphObj;

                // run
                mc.Run();

                IsProcessing = true;
                while (!_stopCameraEvent.WaitOne(0, true))
                {
                    Thread.Sleep(150);
                }

                mc.StopWhenReady();
            }
            // catch any exceptions
            catch (Exception e)
            {
                CameraError = e;
                Console.WriteLine(e.ToString());
                System.Diagnostics.Debug.WriteLine("----: " + e.ToString());
            }
            // finalization block
            finally
            {
                // release all objects                    
                IsProcessing = false;
                mc = null;
                graph = null;
                sourceBase = null;
                grabberBase = null;
                sg = null;

                if (graphObj != null)
                {
                    Marshal.ReleaseComObject(graphObj);
                    graphObj = null;
                }
                if (sourceObj != null)
                {
                    Marshal.ReleaseComObject(sourceObj);
                    sourceObj = null;
                }
                if (grabberObj != null)
                {
                    Marshal.ReleaseComObject(grabberObj);
                    grabberObj = null;
                }

                _cameraCaptureThread = null;
                OnPropertyChanged("CameraCaptureRunning");
            }
        }

        private static void FreeAMMediaType(AMMediaType mediaType)
        {
            if (mediaType != null)
            {
                if (mediaType.formatSize != 0)
                {
                    Marshal.FreeCoTaskMem(mediaType.formatPtr);
                    mediaType.formatSize = 0;
                    mediaType.formatPtr = IntPtr.Zero;
                }
                if (mediaType.unkPtr != IntPtr.Zero)
                {
                    Marshal.Release(mediaType.unkPtr);
                    mediaType.unkPtr = IntPtr.Zero;
                }
            }
        }

        // Open local capture device
        public bool OpenCaptureDevice(LocalVideoDevice videoDevice, out Exception exceptionOut)
        {
            exceptionOut = null;

            try
            {
                AssignCameraNameAndMoniker(videoDevice.DeviceName, videoDevice.DeviceMoniker);
                StartCameraCapture();
            }
            catch (Exception ex)
            {
                exceptionOut = ex;
                return false;
            }

            return true;
        }
    }
}